<template>
    <div>
      <el-card class="box-card">
        <div slot="header" class="clearfix">
          <span>文章列表</span>
        </div>
        <!-- 搜索区域 -->
        <div class="search-box">
          <!-- 设置 inline 属性可以让表单域变为行内的表单域 -->
          <el-form :inline="true" :model="q">
            <el-form-item label="文章分类">
              <el-select v-model="q.cate_id" placeholder="请选择分类" size="small">
                <el-option v-for="obj in cateList" :key="obj.id" :label="obj.cate_name" :value="obj.id"></el-option>
              </el-select>
            </el-form-item>
            <el-form-item label="发布状态" style="margin-left: 15px;">
              <el-select v-model="q.state" placeholder="请选择状态" size="small">
                <el-option label="已发布" value="已发布"></el-option>
                <el-option label="草稿" value="草稿"></el-option>
              </el-select>
            </el-form-item>
            <el-form-item>
              <el-button type="primary" size="small" @click="choseFn">筛选</el-button>
              <el-button type="info" size="small" @click="resetFn">重置</el-button>
            </el-form-item>
          </el-form>
          <!-- 发表文章的按钮 -->
          <el-button type="primary" size="small" class="btn-pub" @click="showPubDialogFn">发表文章</el-button>
        </div>

          <!-- 文章表格区域 -->
          <el-table :data="articleList" style="width: 100%;" border stripe>
            <el-table-column label="文章标题" prop="title">
              <template v-slot="scope">
                <!-- 这里使用了作用域插槽 -->
                <!-- 使得文章标题能够点击 -->
                <el-link type="primary" @click="showDetailFn(scope.row.id)">{{ scope.row.title }}</el-link>
              </template>
            </el-table-column>
            <el-table-column label="分类" prop="cate_name"></el-table-column>
            <el-table-column label="发表时间" prop="pub_date">
              <!-- 这里运用了作用域插槽 -->
              <template v-slot="scope">
                <!-- span中的插值语法中通过调用Vue原型对象上的formatDate方法，并且将pub_date作为参数传递过去，经过formatDate的加工成格式化的日期，最终返回过来并且呈现到页面上 -->
                <span>{{ $formatDate(scope.row.pub_date) }}</span>
              </template>
            </el-table-column>
            <el-table-column label="状态" prop="state"></el-table-column>
            <el-table-column label="操作">
              <!-- 下面用了作用域插槽，并且将scope中的row解构赋值，row是这一行的数据对象 -->
              <template v-slot="{ row }">
                <el-button type="danger" size="mini" @click="removeFn(row.id)">删除</el-button>
              </template>
            </el-table-column>
          </el-table>

        <!-- 分页区域 -->
        <!--
          current-page是当前所在的页数，:current-page.sync="q.pagenum"是将页数双向回传给pagenum，二者相互影响
          page-sizes是每一页能够显示的数据条数
          layout装的是这个分页组件需要的子组件
          @size-change：pageSize 改变时会触发
          @current-change：currentPage 改变时会触发
         -->
        <el-pagination
          @size-change="handleSizeChangeFn"
          @current-change="handleCurrentChangeFn"
          :current-page.sync="q.pagenum"
          :page-sizes="[2, 3, 5, 10]"
          :page-size.sync="q.pagesize"
          layout="total, sizes, prev, pager, next, jumper"
          :total="total"
        >
        </el-pagination>
      </el-card>

      <!-- 发表文章的 Dialog 对话框 -->
      <!-- :before-close="handleClose"的意思是当下面的el-dialog被关闭前就会调用handleClose方法 -->
      <el-dialog title="发表文章"
      :visible.sync="pubDialogVisible"
      fullscreen
      :before-close="handleClose"
      @close="dialogCloseFn">
        <!-- 发布文章的对话框 -->
        <el-form :model="pubForm" :rules="pubFormRules" ref="pubFormRef" label-width="100px">
          <el-form-item label="文章标题" prop="title">
            <el-input v-model="pubForm.title" placeholder="请输入标题"></el-input>
          </el-form-item>
          <el-form-item label="文章分类" prop="cate_id">
            <el-select v-model="pubForm.cate_id" placeholder="请选择分类" style="width: 100%;">
              <el-option v-for="obj in cateList" :key="obj.id" :label="obj.cate_name" :value="obj.id"></el-option>
            </el-select>
          </el-form-item>
          <el-form-item label="文章内容" prop="content">
              <!-- 使用 v-model 进行双向的数据绑定 -->
              <!-- 使用富文本编辑器 -->
              <quill-editor v-model="pubForm.content" @blur="contentChangeFn"></quill-editor>
          </el-form-item>
          <el-form-item label="文章封面" prop="cover_img">
            <!-- 用来显示封面的图片 -->
            <img src="../../assets/images/cover.jpg" alt="" class="cover-img" ref="imgRef" />
            <br />
            <!-- 文件选择框，默认被隐藏 -->
            <input type="file" style="display: none;" accept="image/*" ref="iptFileRef" @change="changeCoverFn" />
            <!-- 选择封面的按钮 -->
            <el-button type="text" @click="selCoverFn">+ 选择封面</el-button>
          </el-form-item>
          <el-form-item>
              <el-button type="primary" @click="pubArticleFn('已发布')">发布</el-button>
              <el-button type="info" @click="pubArticleFn('草稿')">存为草稿</el-button>
          </el-form-item>
        </el-form>
      </el-dialog>

      <!-- 查看文章详情的对话框 -->
      <el-dialog title="文章预览" :visible.sync="detailVisible" width="80%">
        <h1 class="title">{{ artDetail.title }}</h1>

        <div class="info">
          <!-- 如果artDetail中有用户昵称nickname就用nickname，如果没有就用用户名username -->
          <span>作者：{{ artDetail.nickname || artDetail.username }}</span>
          <!-- 调用Vue原型对象上的formatDate方法将后台返回过来的日期数据格式化 -->
          <span>发布时间：{{ $formatDate(artDetail.pub_date) }}</span>
          <span>所属分类：{{ artDetail.cate_name }}</span>
          <span>状态：{{ artDetail.state }}</span>
        </div>
        <!-- 分割线 -->
        <el-divider></el-divider>
        <!-- 文章的封面 -->
        <!--
             后端返回图片链接地址的经验：
             后端返回的图片地址是半截的
             因为服务器的域名可能会来回变化，所以数据库里的地址只有相对路径
             要求：前端请求此图片的时候，后端告诉你文件真身所在的服务器域名，前端自己拼接前缀
         -->
        <img v-if="artDetail.cover_img" alt="" :src="baseURLR + artDetail.cover_img" />
        <!-- 文章的详情 -->
        <!-- 后台返回的artDetail.content里面包含着标签以及标签中的内容，插值语法会把标签和标签中的内容一起当作普通的字符串呈现而不会解析标签，所以下面就要用到v-html，v-html会识别artDetail.content中的标签部分，并且将它解析，最后联通标签内容一起呈现大到页面上 -->
        <div class="detail-box" v-html="artDetail.content"></div>
      </el-dialog>
    </div>
  </template>

<script>
// 引入 baseURL接口和图片资源所在的服务器地址
import { baseURL } from '@/utils/request'
// 引入接口方法
import { getArtCateListAPI, uploadArticleAPI, getArticleListAPI, getArticleDetailFn, delArticleAPI } from '@/api'
// 在标签和样式中，引入图片文件可以直接写“静态路径”，在JS中引入图片要用import引入
// 把路径放在js的vue变量里再赋予回标签和样式是不行的，原因：webpack分析标签的时候，如果src的值是一个相对路径，它会去帮我们找那个路径的文件并一起打包，打包的时候会分析图片的大小，如果小就会转换成base64字符串再赋予给src，如果大就会拷贝图片再换个路径给img的src显示(运行时)
// webpack会将图片变成一个base64字符串或者是这个图片在打包后的临时地址
// Vue变量中的路径，赋予给标签时，都会被当作普通的字符串使用
// 解决方法：如果想要在JS中引入图片地址，就用import引入，让webpack把它当作模板数据，把它转换成打包后的图片路径或base64的字符串
// 注意：如果这个图片不是相对路径本地图片，而是http://外链的图片地址，就可以在标签中直接应用，或者是在js中用变量保存后赋予给标签，而不用import引入(因为在运行时，浏览器发现src地址是外链式的就不会找相对路径的文件夹了)
import imgObj from '../../assets/images/cover.jpg'
export default {
  name: 'ArtList',
  data () {
    return {
      baseURLR: baseURL, // 将引入的baseURl地址赋值给data中的数据，因为template只能使用Vue体系内的变量，不能直接使用从外部引入的数据
      // 查询参数对象
      q: { // 将这个数据通过接口传递给后台，后台根据这里面的参数返回对应的数据
        pagenum: 1, // 默认那第一页的数据，这里的属性值是多少，就默认返回第几页的数据
        pagesize: 2, // 默认每一页只能呈现2条文章数据，这个属性值是多少，就默认返回几条文章数据
        cate_id: '',
        state: ''
      },
      cateList: [], // 保存文章分类列表
      articleList: [], // 保存文章列表
      total: 0, // 保存现有文章的总数
      pubDialogVisible: false, // 控制发布文章对话框的显示(true)和隐藏(false)
      detailVisible: false, // 控制查看文章详情对话框的显示(true)和隐藏(false)
      pubForm: { // 发布文章时表单的数据对象
        title: '', // 文章标题
        cate_id: '', // 文章分类id
        content: '', // 文章内容
        cover_img: '', // 封面图片(保存的是一个文件)
        state: '' // 发布状态(值：'已发布'或'草稿'，这个值是后端要求的，必须是这两个中的一个)
      },
      pubFormRules: { // 发布文章时表单的验证规则对象
        title: [ // el-input等输入框在blur失焦时进行校验
          { required: true, message: '请输入文章标题', trigger: 'blur' },
          { min: 1, max: 30, message: '文章标题的长度为1-30个字符', trigger: 'blur' }
        ],
        // 下拉框，单选框，复选框是在change内容改变时进行校验
        cate_id: [{ required: true, message: '请选择文章标题', trigger: 'change' }],
        // content对应的quill-editor富文本编辑器，它不是el提供的表单标签
        // quill-editor这个标签靠组件标签的blur和change都无法触发校验，只有靠自己手动绑定blur失焦或change内容改变才能触发校验，然后还得在手动设置的change事件回调函数中调用el-form组件对象中的校验规则(validateField)再重新执行
        content: [{ required: true, message: '请输入文章内容', trigger: 'blur' }],
        cover_img: [{ required: true, message: '请选择文章封面', trigger: 'blur' }]
      },
      artDetail: {} // 这个对象用于存储通过接口接收到的指定的文章详情变量
    }
  },
  created () {
    this.getCateListFn() // 当当前组件被创建时就调用getCateListFn获取文章所有分类
    this.getArtcleListFn() // 当当前组件被创建时就调用getArtcleListFn获取文章列表
  },
  methods: {
    // 发表文章按钮的点击事件
    showPubDialogFn () {
      this.pubDialogVisible = true
    },
    // 发布文章的对话框关闭时的回调
    async handleClose (done) { // done被调用后就会关闭对话框
      // 询问用户是否确认关闭对话框
      const confirmResult = await this.$confirm(
        '此操作将导致文章信息丢失, 是否继续?', '提示',
        {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }
      ).catch(err => err) // 捕获确认框Promise对象里选择取消时，拒绝状态结果'cancel'字符串
      // console.log(confirmResult)   // 点击的是确定，这里输出的就是confirm，是取消，这里输出的就是cancel
      // $confirm内部虽然是一个确认提示框，但是它借用了Promise语法来管理，点击确定，它的状态为兑现成功状态返回的'confirm'，如果点击取消按钮，此Promise对象状态为拒绝状态，返回'cancel'字符串

      // 知识点
      // 1. await只能用在被async修饰的函数内
      // async修饰的函数就是异步函数，如果此函数被调用就会返回一个全新的Promise对象
      // 而且由于这个函数是异步函数，所以外面的同步函数继续执行，而await暂停代码只能暂停async函数内的，等待await后面的异步结果
      // 2. await只能拿到成功的结果并放行继续往下走，如果失败，内部就会想浏览器控制台抛出错误，并且不会让await往下走
      // 3. await后面的Promise的对象的拒接状态(错误)如何捕获？
      // 方法一：try{} catch(err){}
      // 方法二：用Promise的链式调用，而且再catch里的return的非promise拒接状态的对象值，都会当做成功的结果返回给原地新的Promise对象结果

      // 取消了关闭-阻止住, 什么都不干，不让下面的done执行，这样就不会关闭对话框
      if (confirmResult === 'cancel') return
      // 确认关闭
      done() // 调用done，关闭对话框
    },
    // 获取所有分类
    async getCateListFn () {
      const { data: res } = await getArtCateListAPI()
      this.cateList = res.data
    },
    // 获取所有的文章列表
    async getArtcleListFn () {
      const { data: res } = await getArticleListAPI(this.q) // 将q作为参数传递给接口，后台根据这个q返回指定的数据
      // console.log(res)
      this.articleList = res.data // 将后台返回的相应的文章列表赋值给articleList(注意：有分页，并不是所有数据)
      this.total = res.total // 将后台返回的相应的文章总数赋值给total
    },
    // 选择封面点击事件，让文件选择窗口出现
    selCoverFn () { // 这里是通过点击button按钮触发input标签的点击事件
      this.$refs.iptFileRef.click() // 用js模拟一次点击事件
    },
    // 具有文章封面预览功能的事件函数
    changeCoverFn (e) {
      // e.target 拿到触发这个事件的标签(这里是input标签，button按钮被点击后，就会模拟input框被点击，input框被点击后会出现文件选择框，选择了文件后input框就会改变，从而change事件触发)
      // e.target.files 拿到选择的文件，它是一个数组形式
      const files = e.target.files
      if (files.length === 0) { // 这里是如果用户没有选择图片，那就把cover_img变量置空
        this.pubForm.cover_img = null
        // 如果用户没有选择图片，那么img标签就显示原来默认的图片
        this.$refs.imgRef.setAttribute('src', imgObj) // imgObj存的是原来默认的img图片
      } else {
        this.pubForm.cover_img = files[0] // 如果用户选择了图片，那就将用户选择的第一张图片文件保存到cover_img中
        // 如果用户选择了图片文件，那么就把图片文件显示到img中
        const url = URL.createObjectURL(files[0])
        this.$refs.imgRef.setAttribute('src', url) // 通过标识符锁定img标签，再用原生js方法setAttribute给这个img标签设置属性，属性名为src，属性值为变量url
      }
      // 以下代码的意思是：当点击了选择封面的按钮，就触发el-form提供的校验方法进行单独校验
      this.$refs.pubFormRef.validateField('cover_img') // 用el-form提供的validateField方法将数据cover_img进行校验
    },
    // 表单中(点击发布/存为草稿)按钮的点击事件
    pubArticleFn (state) { // 这个state接收的是‘已发布’或‘草稿’，具体是哪个取决于用户点击哪一个按钮，那个按钮就会把参数指定的参数传递进来
      this.pubForm.state = state // 将接收的值保存到pubForm统一表单数据对象上
      // 兜底校验，上面的表单组件标签中的校验规则是当当前表单失去焦点时进行校验，但如果直接点击发布按钮，而不点击表单框，这样表单组件没有聚焦，就无法失去焦点，就无法进行校验，所有这里需要一个兜底校验
      this.$refs.pubFormRef.validate(async valid => {
        if (valid) {
          // 表单中的校验都通过了
          console.log(this.pubForm)
          // 由于后端要求这些数据要用FormData的形式通过接口传递过去，所以下面用来FormData
          const fd = new FormData() // 准备一个表单数据对象的容器 FormData类是HTML5新增的专门为了装文件内容和其他参数的容器
          // 语法：fd.append('参数名',值)
          fd.append('title', this.pubForm.title)
          fd.append('cate_id', this.pubForm.cate_id)
          fd.append('content', this.pubForm.content)
          fd.append('cover_img', this.pubForm.cover_img)
          fd.append('state', this.pubForm.state)

          // 发起请求
          const { data: res } = await uploadArticleAPI(fd) // 这里的fd就是FormData的形式，装的是后端要的数据
          if (res.code !== 0) return this.$message.error('发布文章失败！')
          this.$message.success('发布文章成功！')

          // 关闭对话框
          this.pubDialogVisible = false
          // 刷新文章列表
          this.getArtcleListFn() // 当用户点击了确认发布文章并且发布成功后，通过调用getArtcleListFn方法，重新获取文章列表数据，从而可以更新页面文章列表
        } else {
          return false // 校验未通过，阻止默认行为(按钮有默认的提交行为)
        }
      })
    },
    // 富文本编辑器quill-editor标签绑定的blur事件回调，目的是为了触发quill-editor的校验
    contentChangeFn () {
      this.$refs.pubFormRef.validateField('content') // 用el-form提供的validateField方法将数据content进行校验
    },
    // 新增文章对话框关闭时清空按规划表单
    dialogCloseFn () {
      this.$refs.pubFormRef.resetFields() // 通过resetFields()重置表单标签
      // 对话框中的img标签不受resetFields()的影响，所以得手动清空img标签(将img图片更换成原来的默认图片)
      this.$refs.imgRef.setAttribute('src', imgObj)
    },
    // 分页->每页条数发生改变时触发的事件回调，这个和下面那个都是el-pagination组件标签内置的事件回调
    handleSizeChangeFn (sizes) {
      // sizes：当前需要每一页显示的条数
      // 因为Pagination的标签上已经加了:page-size.sync="q.pagesize"，两种数据进行了双向数据绑定，所以当这个方法被调用了，就说明pageSize改变了，跟它双向数据绑定的q.pagesize也随之改变
      this.q.pagesize = sizes // 所以这一步是没有必要的，当然加了也没影响
      this.q.pagenum = 1
      // 重新向后台发一次请求文章列表的请求，进而更新文章列表
      this.getArtcleListFn()
    },
    // 分页->当前的页码发生改变时触发的事件回调
    handleCurrentChangeFn (nowPage) {
      // nowPage：当前要看的是第几页，页数
      // 这两步跟上面那个方法中的两步一样
      this.q.pagenum = nowPage
      this.getArtcleListFn()
    },
    // 筛选按钮的点击事件回调
    choseFn () {
      // 效果：当点击了筛选按钮，那页码回归为1，每一页的文章条数回归为2
      this.q.pagenum = 1
      this.q.pageSize = 2
      this.getArtcleListFn()
    },
    // 重置按钮的点击事件回调
    resetFn () {
      this.q.pagenum = 1
      this.q.pageSize = 2
      this.q.cate_id = ''
      this.q.state = ''
      this.getArtcleListFn()
    },
    // 文章标题的点击事件回调，点击后可以查看到文章详情
    async showDetailFn (articleId) {
      // articleId是当前文章的id值
      const { data: res } = await getArticleDetailFn(articleId) // 将当前文章的id值通过接口传递给后台，后台根据id值返回指定文章的详情
      if (res.code !== 0) return this.$message.error('获取文章详情失败!')
      this.artDetail = res.data
      // console.log(this.artDetail)
      // 展示对话框
      this.detailVisible = true
    },
    // 删除文章按钮的点击事件
    async removeFn (artId) { // artId接收的是要删除的文章的id
      const { data: res } = await delArticleAPI(artId)
      if (res.code !== 0) return this.$message.error(res.message)
      this.$message.success('删除文章成功')
      // this.resetFn() // 删除文章成功后就把分页和筛选条件重置，让表格数据重新请求一次

      // BUG：在最后一页删除最后一条的时候，虽然页码能回到上一页，但是上一页中本因出现的数据没有出现
      // 原因：在network中发现q.pagenum的值不会自己回到上一页，因为你的代码没有修改过这个q.pagenum值，只是调用了getArtcleListFn方法，带着之前的参数请求去了，所以上一页没数据
      // 解决：在调用接口以后，刷新数组列表之前，对页码进行处理
      if (this.articleList.length === 1) {
        if (this.q.pagenum > 1) { // 保证pagenum的最小值为1
          this.q.pagenum--
        }
      }
      this.getArtcleListFn() // 删除文章成功后重新获取文章列表，实现文章表格数据刷新
    }
  }
}
</script>

  <style lang="less" scoped>
   // 设置富文本编辑器的默认最小高度
   // ::v-deep作用: 穿透选择, 正常style上加了scoped的话, 会给.ql-editor[data-v-hash]属性, 只能选择当前页面标签或者组件的根标签
   // 如果想要选择组件内的标签(那些标签没有data-v-hash值)所以正常选择选不中, 加了::v-deep空格前置的话, 选择器就会变成如下形式
   // [data-v-hash] .ql-editor 这样就能选中组件内的标签的class类名了
   // 总结：scoped不会给组件内的标签添加data-v属性，所以要用 ::v-deep 穿透选择组件内的标签设置样式
   ::v-deep .ql-editor {
    // 这是设置最小高度：标签本身的高度靠内容撑开，标签中有内容且将高度撑开到300px以上时，下面的代码就不会生效，该标签的高度就是它内容撑开的高度；标签没有内容时，高度不够300px时，下面的代码就会直接设置高度为300px
     min-height: 300px;
   }

  .search-box {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    .btn-pub {
      margin-top: 5px;
    }
  }

   // 设置图片封面的宽高
   .cover-img {
     width: 400px;
     height: 280px;
     object-fit: cover;
   }

   .title {
     font-size: 24px;
     text-align: center;
     font-weight: normal;
     color: #000;
     margin: 0 0 10px 0;
   }
   .info {
     font-size: 12px;
     span {
       margin-right: 20px;
     }
   }
   // 修改 dialog 内部元素的样式，需要添加样式穿透
   ::v-deep .detail-box {
     img {
       width: 500px;
     }
   }
  </style>
